Decorating symbols
Giving a rich visual representation to a symbol while programming elevates the whole user experience to another level. There are a couple of ways to do this in the WLJS Notebook.
Read about syntax sugar in Symbolic programming.
Temporal
You can replace a symbol with an icon by applying Interpretation. After the first evaluation, the representation is lost and the true expression is revealed:
Interpretation[Graphics[Circle[], ImageSize->{20,20}, ImagePadding->None], 1]
The result is a circle symbol, which can be copied multiple times in any cell.
The decoration itself does not modify the original expression. You can check this by pasting the result into any text editor:
(*VB[*)(1)(*,*)(*"1:eJxTTM..."*)(*]VB*) - 1
Examples in action
A navigation gizmo snippet is made using this technique combined with dynamically generated symbols wrapped in Offload.
Details
Using JavaScript
You can use the full power of the web to decorate your symbols. For example, create a JavaScript cell with the following content:
core.SmileyDecorator = async (args, env) => {
const canvas = document.createElement('canvas');
canvas.width = 50;
canvas.height = 50;
const ctx = canvas.getContext('2d');
// Draw a smiley face
ctx.beginPath();
ctx.arc(25, 25, 20, 0, Math.PI * 2, true); // Outer circle
ctx.moveTo(35, 25);
ctx.arc(25, 25, 8, 0, Math.PI, false); // Mouth
ctx.moveTo(22, 20);
ctx.arc(20, 20, 2, 0, Math.PI * 2, true); // Left eye
ctx.moveTo(32, 20);
ctx.arc(30, 20, 2, 0, Math.PI * 2, true); // Right eye
ctx.stroke();
env.element.appendChild(canvas);
}
This function appends a canvas with an image to the provided element. To force the Wolfram Kernel to execute this symbol on the frontend, use ViewBox:
SmileyDecorator /: MakeBoxes[s_SmileyDecorator, StandardForm] := With[{},
ViewBox[s, s]
]
SmileyDecorator[]
Now we can apply a similar trick:
Interpretation[SmileyDecorator[], 1]
Magic joystick
We can hide all event bindings from the user by providing a symbol that can be used in dynamic expressions:
createDynamic2DSymbol[] := Module[{symbol = {0,0}}, With[{
eventObject = InputJoystick[],
helper = InputJoystick`IntegrationHelper[][Function[xy, symbol = xy]]
},
EventHandler[eventObject, helper];
Interpretation[eventObject, Offload[symbol]]
]]
You can create an instance by evaluating the following in a new cell:
createDynamic2DSymbol[]
This outputs a joystick:
Now cut and paste it into the cell below:
Plot[Sin[x], {x, -5Pi, 5Pi}, Epilog->{
Disk[ (* paste it here *), 0.5]
}]
Once evaluated, you'll get a controllable Disk using the joystick inside the input cell:
A slightly more optimized version follows...
Details
Here, we do not spawn an additional wrapper widget. The result is the same but with less overhead on the editor:
createDynamic2DSymbol[] := Module[{symbol = {0,0}}, With[{
eventObject = InputJoystick[],
helper = InputJoystick`IntegrationHelper[][Function[xy, symbol = xy]]
},
EventHandler[eventObject, helper];
With[{display = eventObject[[1]]["View"] // CreateFrontEndObject},
Interpretation[display, Offload[symbol]]
]
]]
Why? See the reference on Interpretation.
Permanent
In this approach, RGBColor, most mathematical equations, Graphics, and other visual syntactic sugars are implemented. For example:
Now
Red
InterpretationBox
This is a low-level symbol used by Interpretation. However, we can make a permanent version from temporal decoration by defining MakeBoxes for StandardForm.
Advantages ✅
- High-level, accepts anything as a display expression
- Compatible with Mathematica
Drawbacks ❌
- Slightly heavier than ViewBox; in fact, a wrapper around it
- Cannot use JavaScript decorators
Neutral 💭
- Immutable
- Preserves the original expression in the cell
Basic example
boxObject[_Real]
Let's decorate it:
boxObject /: MakeBoxes[boxObject[s_], form: StandardForm] := With[{
g = Graphics[{Blue, Disk[{0,0},1], Opacity[0.5], Red, Disk[{0,0},s]}, ImageSize->80, Controls->False, ImagePadding->None]
},
InterpretationBox[MakeBoxes[g, form], boxObject[s]]
]
Test it:
boxObject[3.2]
Morse code
Let's create syntax sugar for Morse code. Our special symbol will be:
morse[code_String]
Define conversion rules:
morseTable = {"a" -> ".- ", "b" -> "-... ", "c" -> "-.-. ",
"d" -> "-.. ", "e" -> ". ", "f" -> "..-. ", "g" -> "--. ",
"h" -> ".... ", "i" -> ".. ", "j" -> ".--- ", "k" -> "-.- ",
"l" -> ".-.. ", "m" -> "-- ", "n" -> "-. ", "o" -> "--- ",
"p" -> ".--. ", "q" -> "--.- ", "r" -> ".-. ", "s" -> "... ",
"t" -> "- ", "u" -> "..- ", "v" -> "...- ", "w" -> ".-- ",
"x" -> "-..- ", "y" -> "-.-- ", "z" -> "--.. ", " " -> "/ "};
ToMorseCode[text_String] := StringReplace[ToLowerCase[text], morseTable];
morse /: TextString[morse[s_String]] := s;
Now create boxes for it:
morse /: MakeBoxes[m: morse[s_], f:StandardForm] := With[{
code = ToMorseCode[s]
},
InterpretationBox[MakeBoxes[Style[code, 18, Italic], f], m]
]
Test it:
morse["SOS"]
Back transformation is possible because the original expression is preserved:
Finally, make it audible:
silence = Table[0, {t,0,40Pi,0.1}];
dot = Table[Sin[5 t], {t,0,40Pi,0.1}];
dash = Join[dot, dot];
morse /: Play[morse[t_String]] := With[{
code = ToMorseCode[t]
},
Join @@ (Switch[#, ".", Join[dot, silence], "-", Join[dash, silence], _, Join[silence, silence]] & /@ StringSplit[code, ""]) // ListPlay
];
morse["SOS"] // Play
ArrangeSummaryBox
This is another built-in function useful for representing objects.
Advantages ✅
- High-level and easy to use
- Compatible with Mathematica
Drawbacks ❌
- Primarily displays text fields and a single icon
Neutral 💭
- Practically immutable
- Preserves the original expression in the cell
If you want to hear more about OOP-like objects in Wolfram Language, check out this guide: Creating new type.
For example, we have a symbol with information inside its arguments:
specialSymbol[<|"Date" -> Now, "Color" -> Red, "State" -> True|>]
Let's decorate it:
specialSymbol /: MakeBoxes[obj : specialSymbol[asc_Association], StandardForm] :=
Module[{above},
above = {
{BoxForm`SummaryItem[{"Date: ", asc["Date"]}]},
{BoxForm`SummaryItem[{"Color: ", asc["Color"]}]},
{BoxForm`SummaryItem[{"State: ", asc["State"]}]}
};
BoxForm`ArrangeSummaryBox[
specialSymbol,
obj,
None,
above,
Null
]
];
The result looks like this:
specialSymbol[<|"Date" -> Now, "Color" -> Red, "State" -> True|>]
This symbol is still valid for evaluation; what you see is only syntax sugar.
Now add an icon based on the "Color"
field:
specialSymbol /: MakeBoxes[obj : specialSymbol[asc_Association], StandardForm] :=
Module[{above, icon},
above = {
{BoxForm`SummaryItem[{"Date: ", asc["Date"]}]},
{BoxForm`SummaryItem[{"Color: ", asc["Color"]}]},
{BoxForm`SummaryItem[{"State: ", asc["State"]}]}
};
icon = Graphics[{
Lighter[asc["Color"]], Disk[{0,0}, 1],
asc["Color"], Disk[{0,0}, 0.8]
},
ImageSize -> {50, 50},
ImagePadding -> None,
Controls -> False,
PlotRange -> {{-1,1},{-1,1}}
];
BoxForm`ArrangeSummaryBox[
specialSymbol,
obj,
icon,
above,
Null
]
];
See how to make dynamic decorations in the guide: Creating new type.
ViewBox
A low-level building block used by Interpretation
, InterpretationBox
, ArrangeSummaryBox
, and others.
Advantages ✅
- Fully customizable
- Can emit events
- Usually the fastest approach
- Saves memory—no frontend object is created by default
Drawbacks ❌
- Requires a symbol defined as WLJS Functions
- Not compatible with Mathematica
Neutral 💭
- Mutable (see Mutability)
- Preserves the original expression in the cell
Simple example
The easiest way to use it is to replace an expression with graphics, an image, or something similar:
boxObject[_Real]
Let's decorate it:
boxObject /: MakeBoxes[boxObject[s_], StandardForm] := With[{
g = Graphics[{Blue, Disk[{0,0},1], Opacity[0.5], Red, Disk[{0,0},s]}, ImageSize -> 80, Controls -> False, ImagePadding -> None]
},
ViewBox[boxObject[s], g]
]
Normal expressions won't work as a display value in ViewBox
. Use existing types like Graphics, Graphics3D, or Image. Otherwise, define them as WLJS Functions.
If your display expression is large, consider using CreateFrontEndObject
on g
before passing it to ViewBox
. This stores the data in shared memory.
Table[boxObject[i], {i, 3}]
You can also trigger a frontend beep when the widget is destroyed:
boxObject /: MakeBoxes[boxObject[s_], StandardForm] := With[{
g = Graphics[{Blue, Disk[{0,0},1], Opacity[0.5], Red, Disk[{0,0},s]}, ImageSize -> 80, Controls -> False, ImagePadding -> None],
uid = CreateUUID[]
},
EventHandler[uid, {"Destroy" -> Beep}];
ViewBox[boxObject[s], g, "Event" -> uid]
]
External decorators 1
You can also use JavaScript to decorate a symbol. We'll rewrite MakeBoxes
for it:
boxObject /: MakeBoxes[boxObject[s_], StandardForm] := With[{},
ViewBox[boxObject[s], customDecorator[s]]
]
We didn't use CreateFrontEndObject here since our decorator is simple and lightweight.
Now define the decorator function:
.js
core.customDecorator = async (args, env) => {
const state = await interpretate(args[0], env);
const element = env.element;
element.classList.add('flex', 'rounded-md', 'p-2');
element.style.border = "1px solid #999";
element.style.boxShadow = "inset 0 2px 4px 0 rgb(0 30% 0 / 0.05)";
element.style.transitionDuration = '0.8s';
element.style.transitionProperty = 'transform';
setTimeout(() => {
element.style.transform = "rotate(360deg)";
}, 100);
element.innerText = state;
}
Test it:
boxObject[33]
You can also make it dynamic by defining a .update
method for customDecorator
. (See WLJS Functions)
External decorators 2
Adapted from the Full interpretation section.
Define a gauge meter:
gauge[level_Real]
Decorate it like before:
gauge /: MakeBoxes[g_gauge, StandardForm] := With[{},
ViewBox[g, g]
]
For use with FrontSlidesSelected or WLX, define WLXForm
in MakeBoxes
.
Decorator implementation:
.js
core.gauge = async (args, env) => {
const gauge = document.createElement('div');
gauge.style.width = '100px';
gauge.style.height = '50px';
gauge.style.border = '1px solid #000';
gauge.style.borderRadius = '50px 50px 0 0';
gauge.style.position = 'relative';
gauge.style.background = 'linear-gradient(to right, red 0%, yellow 50%, green 100%)';
const needle = document.createElement('div');
needle.style.width = '2px';
needle.style.height = '40px';
needle.style.background = '#000';
needle.style.position = 'absolute';
needle.style.bottom = '0';
needle.style.left = '50%';
needle.style.transformOrigin = 'bottom';
function setNeedlePosition(value) {
value = Math.max(0, Math.min(1, value));
const angle = value * 180 - 90;
needle.style.transform = `rotate(${angle}deg)`;
}
const pos = await interpretate(args[0], env);
setNeedlePosition(pos);
gauge.appendChild(needle);
env.element.appendChild(gauge);
}
gauge[0.3]
Even when copied to a plain text editor, the original symbol remains:
(*VB[*)(gauge[Offload[gvalue]])(*,*)(*"1:eJxTTMoPSmNkYGAoZgESHvk5KRAeP5BwK8rPK3HNSwnLLCopTcyBSLACifTE0vRUCJcdSPinpeXkJ6YUs4GkyhJzSlMBOCoUGw=="*)(*]VB*)
Updates
Make the gauge dynamic:
core.gauge = async (args, env) => {
const gauge = document.createElement('div');
gauge.style.width = '100px';
gauge.style.height = '50px';
gauge.style.border = '1px solid #000';
gauge.style.borderRadius = '50px 50px 0 0';
gauge.style.position = 'relative';
gauge.style.background = 'linear-gradient(to right, red 0%, yellow 50%, green 100%)';
const needle = document.createElement('div');
needle.style.width = '2px';
needle.style.height = '40px';
needle.style.background = '#000';
needle.style.position = 'absolute';
needle.style.bottom = '0';
needle.style.left = '50%';
needle.style.transformOrigin = 'bottom';
function setNeedlePosition(value) {
value = Math.max(0, Math.min(1, value));
const angle = value * 180 - 90;
needle.style.transform = `rotate(${angle}deg)`;
}
const pos = await interpretate(args[0], env);
setNeedlePosition(pos);
gauge.appendChild(needle);
env.element.appendChild(gauge);
env.local.update = setNeedlePosition;
}
core.gauge.update = async (args, env) => {
const val = await interpretate(args[0], env);
env.local.update(val);
}
core.gauge.destroy = () => {
console.log('Nothing to do');
}
core.gauge.virtual = true;
gauge /: MakeBoxes[g_gauge, StandardForm] := With[{},
ViewBox[g, g]
]
Test with a slider:
gvalue = 0.1;
EventHandler[InputRange[0, 1, 0.1, 0.1], (gvalue = #) &];
gauge[gvalue // Offload]
Deferred
The key difference here is that the decoration is created only when the symbol appears in the editor. For this, we construct a dummy ViewBox to emit a mount event:
dummy /: MakeBoxes[dummy[handler_], StandardForm] := With[{
uid = CreateUUID[]
},
EventHandler[uid, {"Mounted" :> Function[ref,
FrontSubmit[handler[ref], FrontInstanceReference[ref]]
]}];
ViewBox[Null, Null, "Event" -> uid]
]
The handler
function populates the instance of ViewBox
with content. Here's a simple example generating random shapes:
handler[ref_] := With[{
g = With[{d = 2 Pi/RandomInteger[{2, 16}]},
Graphics[
Table[{
EdgeForm[Opacity[.6]], Hue[(-11 + q + 10 r)/72, 1, 1],
Polygon[{
(8 - r) {Cos[d (q - 1)], Sin[d (q - 1)]},
(8 - r) {Cos[d (q + 1)], Sin[d (q + 1)]},
(10 - r) {Cos[d q], Sin[d q]}
}]
}, {r, 6}, {q, 12}],
ImageSize -> {50, 50}, ImagePadding -> None
]
}
},
g
]
Try it:
dummy[handler]
Even if the widget is copied, it will still be a unique instance. The handler
runs whenever the symbol appears in the editor.
Use deferred decoration if you need each instance to be unique—even when copied.
State Preservation
Where do you store the state? Use ViewBox`InnerExpression
to keep data inside the cell.
Example with sliders:
handler[state_String, ref_, window_] := Module[{
object = InputRange[0,1, 0.1, ToExpression[state]]
},
EventHandler[object, Function[value,
FrontSubmit[ViewBox`InnerExpression[ToString[value]], ref];
]];
object[[1, "View"]] // CreateFrontEndObject
]
slider /: MakeBoxes[slider[initial_:0.5], StandardForm] := With[{
uid = CreateUUID[]
},
EventHandler[uid, {"Mounted" :> Function[assoc,
Then[
FrontFetchAsync[ViewBox`InnerExpression[], FrontInstanceReference[assoc["Instance"]]],
Function[payload,
FrontSubmit[
handler[First@Flatten@{payload}, FrontInstanceReference[assoc["Instance"]]],
FrontInstanceReference[assoc["Instance"]]
]
]
]
]}];
ViewBox[initial, Null, "Event" -> uid]
]
Try it:
slider[0.7]
When you drag the slider, it updates the original value hidden behind the decoration. Copying and pasting generates unique sliders: